/*
 * Copyright 2018 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ait.lienzo.client.widget.panel.impl;

import com.ait.lienzo.client.core.shape.Layer;
import com.ait.lienzo.client.core.shape.Viewport;
import com.ait.lienzo.client.core.types.Point2D;
import com.ait.lienzo.client.core.types.Transform;
import com.ait.lienzo.client.widget.panel.Bounds;
import com.ait.lienzo.client.widget.panel.BoundsProvider;
import com.ait.lienzo.client.widget.panel.LienzoBoundsPanel;
import com.ait.lienzo.client.widget.panel.LienzoPanel;
import com.ait.lienzo.client.widget.panel.event.*;
import com.ait.lienzo.client.widget.panel.scrollbars.ScrollBars;
import com.ait.lienzo.client.widget.panel.scrollbars.ScrollablePanel;
import com.ait.tooling.common.api.java.util.function.Supplier;
import com.ait.tooling.nativetools.client.event.HandlerRegistrationManager;
import com.google.gwt.dom.client.Style;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;

import java.util.Objects;

public class PreviewPanel extends ScalablePanel
{
    private final HandlerManager             m_events;

    private final HandlerRegistrationManager handlers;

    private final Bounds                     visibleBounds;

    private final Point2D                    visibleScaleFactor;

    private final PreviewLayer               previewLayer;

    private final PreviewLayerDecorator      decorator;

    public PreviewPanel(final int width,
                        final int height)
    {
        this(new LienzoPanelImpl(width, height), new HandlerRegistrationManager());
    }

    PreviewPanel(final LienzoPanel panel,
                 final HandlerRegistrationManager registrationManager)
    {
        super(panel, new PreviewBoundsProvider());
        m_events = new HandlerManager(this);
        visibleBounds = Bounds.empty();
        visibleScaleFactor = new Point2D(1, 1);
        handlers = registrationManager;
        previewLayer = new PreviewLayer(backgroundBoundsSupplier,
                                        visibleBoundsSupplier);
        decorator = new PreviewLayerDecorator(handlers,
                                              backgroundBoundsSupplier,
                                              visibleBoundsSupplier,
                                              new PreviewLayerDecorator.EventHandler()
                                              {
                                                  @Override
                                                  public void onMouseEnter()
                                                  {
                                                      getLienzoPanel().getElement().getStyle().setCursor(Style.Cursor.MOVE);
                                                  }

                                                  @Override
                                                  public void onMouseExit()
                                                  {
                                                      getLienzoPanel().getElement().getStyle().setCursor(Style.Cursor.DEFAULT);
                                                  }

                                                  @Override
                                                  public void onMove(final Point2D point)
                                                  {
                                                      setVisibleBoundsAt(point.getX(),
                                                                         point.getY());
                                                      fireLienzoPanelScrollEvent();
                                                      batch();
                                                  }
                                              });
    }

    PreviewPanel(final LienzoPanel panel,
                 final PreviewLayer previewLayer,
                 final PreviewLayerDecorator decorator,
                 final HandlerManager m_events,
                 final HandlerRegistrationManager registrationManager)
    {
        super(panel, new PreviewBoundsProvider());
        this.m_events = m_events;
        this.visibleBounds = Bounds.empty();
        this.visibleScaleFactor = new Point2D(1, 1);
        this.handlers = registrationManager;
        this.previewLayer = previewLayer;
        this.decorator = decorator;
    }

    private final Supplier<Bounds> backgroundBoundsSupplier = new Supplier<Bounds>()
    {
        @Override
        public Bounds get()
        {
            return getBackgroundBounds();
        }
    };

    private final Supplier<Bounds> visibleBoundsSupplier    = new Supplier<Bounds>()
    {
        @Override
        public Bounds get()
        {
            if (!isDisplayVisibleArea())
            {
                return Bounds.empty();
            }
            return Bounds.build(visibleBounds.getX(),
                                visibleBounds.getY(),
                                getVisibleWidth(),
                                getVisibleHeight());
        }
    };

    public PreviewPanel observe(final ScrollablePanel panel)
    {
        getPreviewBoundsProvider().delegate(this, panel);

        handlers.register(
                panel.addLienzoPanelScrollEventHandler(new LienzoPanelScrollEventHandler()
                {
                    @Override
                    public void onScroll(LienzoPanelScrollEvent event)
                    {
                        if (!decorator.isDragging())
                        {
                            scroll(event.getPctX(), event.getPctY());
                        }
                    }
                })
                         );

        handlers.register(
                panel.addLienzoPanelResizeEventHandler(new LienzoPanelResizeEventHandler()
                {
                    @Override
                    public void onResize(LienzoPanelResizeEvent event)
                    {
                        resize(event.getWidth(), event.getHeight());
                    }
                })
                         );

        handlers.register(
                panel.addLienzoPanelScaleChangedEventHandler(new LienzoPanelScaleChangedEventHandler()
                {
                    @Override
                    public void onScale(LienzoPanelScaleChangedEvent event)
                    {
                        if (!decorator.isDragging())
                        {
                            final Point2D factor = event.getFactor();
                            visibleScaleFactor
                                    .setX(1 / factor.getX())
                                    .setY(1 / factor.getY());
                        }
                    }
                }));

        handlers.register(
                panel.addLienzoPanelBoundsChangedEventHandler(new LienzoPanelBoundsChangedEventHandler()
                {
                    @Override
                    public void onBoundsChanged(LienzoPanelBoundsChangedEvent event)
                    {
                        refresh();
                    }
                }));
        handlers.register(
                addLienzoPanelScrollEventHandler(new LienzoPanelScrollEventHandler()
                {
                    @Override
                    public void onScroll(LienzoPanelScrollEvent event)
                    {
                        panel.getScrollHandler().updateLienzoPosition(event.getPctX(),
                                                                      event.getPctY());
                        panel.getScrollHandler().refresh();
                    }
                }));

        // Use actual panel's size.
        resize(panel.getWidthPx(),
               panel.getHeightPx());

        // Use actual panel's scroll position.
        final ScrollBars scrollBars = panel.getScrollHandler().scrollBars();
        scroll(scrollBars.getHorizontalScrollPosition(),
               scrollBars.getVerticalScrollPosition());

        return this;
    }

    @Override
    public LienzoBoundsPanel set(final Layer layer)
    {
        super.set(layer);
        getLienzoPanel().add(previewLayer);
        previewLayer.add(decorator.asPrimitive());
        return this;
    }

    public final HandlerRegistration addLienzoPanelScrollEventHandler(final LienzoPanelScrollEventHandler handler)
    {
        Objects.requireNonNull(handler);

        return m_events.addHandler(LienzoPanelScrollEvent.TYPE, handler);
    }

    void fireLienzoPanelScrollEvent()
    {
        final Bounds backgroundBounds = getBackgroundBounds();
        final double bgWidth          = backgroundBounds.getWidth();
        final double bgHeight         = backgroundBounds.getHeight();
        final double x                = visibleBounds.getX();
        final double y                = visibleBounds.getY();
        final double width            = bgWidth - getVisibleWidth();
        final double height           = bgHeight - getVisibleHeight();
        final double pctX             = width > 0 ? x / width * 100 : 0d;
        final double pctY             = height > 0 ? y / height * 100 : 0d;
        m_events.fireEvent(new LienzoPanelScrollEvent(pctX, pctY));
    }

    public ScalablePanel adjustVisibleBounds(final double pctX,
                                             final double pctY)
    {
        final Bounds backgroundBounds = getBackgroundBounds();
        final double width            = backgroundBounds.getWidth() - getVisibleWidth();
        final double height           = backgroundBounds.getHeight() - getVisibleHeight();
        final double incX             = width * pctX / 100;
        final double incY             = height * pctY / 100;
        final double x                = backgroundBounds.getX();
        final double y                = backgroundBounds.getY();
        return setVisibleBoundsAt(x + incX,
                                  y + incY);
    }

    public ScalablePanel setVisibleBoundsAt(final double x,
                                            final double y)
    {
        visibleBounds
                .setX(x)
                .setY(y);
        return this;
    }

    public ScalablePanel setVisibleBoundsSize(final double width,
                                              final double height)
    {
        visibleBounds
                .setWidth(width)
                .setHeight(height);
        return this;
    }

    private void resize(final double width,
                        final double height)
    {
        setDefaultBounds(Bounds.relativeBox(width, height));
        setVisibleBoundsSize(width,
                             height);
        refresh();
    }

    private void scroll(final double pctX,
                        final double pctY)
    {
        adjustVisibleBounds(pctX,
                            pctY);
        batch();
    }

    @Override
    public void batch()
    {
        super.batch();
        if (null != getLayer())
        {
            decorator.update();
            previewLayer.batch();
        }
    }

    @Override
    protected void doDestroy()
    {
        getPreviewBoundsProvider().destroy();
        decorator.destroy();
        previewLayer.clear();
        handlers.removeHandler();
        super.doDestroy();
    }

    PreviewBoundsProvider getPreviewBoundsProvider()
    {
        return (PreviewBoundsProvider) getBoundsProvider();
    }

    Bounds getVisibleBounds()
    {
        return visibleBounds;
    }

    public static class PreviewBoundsProvider implements BoundsProvider
    {
        LienzoBoundsPanel delegate;

        public PreviewBoundsProvider delegate(final PreviewPanel panel,
                                              final LienzoBoundsPanel delegate)
        {
            this.delegate = delegate;
            return this;
        }

        @Override
        public Bounds get(Layer layer)
        {
            if (null != delegate)
            {
                return delegate.getLayerBounds();
            }
            return Bounds.empty();
        }

        public void destroy()
        {
            this.delegate = null;
        }
    }

    private double getVisibleWidth()
    {
        return visibleBounds.getWidth() * visibleScaleFactor.getX();
    }

    private double getVisibleHeight()
    {
        return visibleBounds.getHeight() * visibleScaleFactor.getY();
    }

    private boolean isDisplayVisibleArea()
    {
        final double visibleWidth  = getVisibleWidth();
        final double visibleHeight = getVisibleHeight();
        final double boundsWidth   = getBounds().getWidth();
        final double boundseHeight = getBounds().getHeight();
        return !(boundsWidth <= visibleWidth && boundseHeight <= visibleHeight);
    }

    private Bounds getBackgroundBounds()
    {
        return obtainViewportBounds(getLayer());
    }

    static double[] getSafeScaleValues(final Layer layer)
    {
        final Viewport  vp        = layer.getViewport();
        final Transform transform = vp.getTransform();
        final double    scaleX    = null != transform ? transform.getScaleX() : 1d;
        final double    scaleY    = null != transform ? transform.getScaleY() : 1d;
        return new double[]{scaleX, scaleY};
    }

    static Bounds obtainViewportBounds(final Layer layer)
    {
        final double[] scale = getSafeScaleValues(layer);
        final Viewport vp    = layer.getViewport();
        final double   vw    = vp.getWidth() * (1 / scale[0]);
        final double   vh    = vp.getHeight() * (1 / scale[1]);
        return Bounds.relativeBox(vw, vh);
    }
}
